-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

-- Description: 异常场景的恢复
local class = require "mc.class"
local log = require 'mc.logging'
local skynet = require 'skynet'
local fructl_handler = require 'infrastructure.fructl'
local power_lock = require 'infrastructure.power_lock'
local upgrade_state_machine = require 'service.upgrade_state_machine'
local gold_package = require 'domain.bios_firmware.gold.gold_package'
local bios_enum = require 'domain.bios_firmware.defs'
local bios_cfg = require 'service.bios_service'
local singleton = require 'mc.singleton'
local bios_factory = require 'factory.bios_factory'
local msg = require 'bios.ipmi.ipmi_message'
local obj_def = require 'macros.object_def'
local ipmi = require 'ipmi'
local comp_code = ipmi.types.Cc
local PfrService = class()

local FRUCTL_LOCK_TIME<const> = 3000
local FORCE_OFF_TIMES<const> = 50
local START_FINISH<const> = 254
local GOLD_INVALID<const> = 0
local SYSTEM_ID<const> = 1
local LOCK_FOREVER<const> = 0xFFFF

function PfrService:ctor(db)
    self.db = db
    -- 是否真正检测
    self.verifing = false
    self.running = {}
    self.state_machine = upgrade_state_machine.get_instance()
    self.gold_package_collection = {}
    self.power_lock_collection = {}
    self.forever_lock_collection = {}
end

function PfrService:delete_gold_package(system_id)
    local gold_obj = self:get_gold_package(system_id)
    if not gold_obj then
        return
    end
    self.gold_package_collection[system_id] = nil
end

function PfrService:add_gold_package(system_id)
    if self.gold_package_collection[system_id] then
        return
    end
    self.gold_package_collection[system_id] = gold_package.new(system_id)
end

function PfrService:get_gold_package(system_id)
    return self.gold_package_collection[system_id]
end

function PfrService:get_gold_package_collection()
    return self.gold_package_collection
end

function PfrService:get_power_lock(system_id)
    if not self.power_lock_collection[system_id] then
        self.power_lock_collection[system_id] = power_lock.new(system_id)
    end
    return self.power_lock_collection[system_id]
end

-- 等待下电，最多重复3次
function PfrService:wait_until_power_off(system_id)
    for i = 1, 3 do
        local power_status = fructl_handler.get_power_status(system_id)
        if power_status == 'OFF' then
            return true
        end
        skynet.sleep(150)
    end
    return false
end

function PfrService:try_force_power_off()
    local times = FORCE_OFF_TIMES
    while times > 0  do
        times = times - 1
        local res = fructl_handler.set_power_state("ForceOff", "Unknown")
        if res then
            return true
        end
        skynet.sleep(200)
    end
    return false
end

-- 1、强制下电
-- 2、解锁
function PfrService:try_unlock_forever(system_id)
    log:notice('[PfrService]PfrService: system %s try unlock fructl forever lock.', system_id)
    local lock

    local system_id = system_id or SYSTEM_ID
    if self.forever_lock_collection[system_id] then
        lock = self.forever_lock_collection[system_id]
    else
        lock = power_lock.new(system_id)
        self.forever_lock_collection[system_id] = lock
        lock:set_reason('VerifyFlash')
        lock:set_lock_time(LOCK_FOREVER)
    end
    skynet.sleep(50)
    lock:unlock()
    self.forever_lock_collection[system_id] = nil
    log:notice('[PfrService]PfrService: system %s unlock fructl forever lock success.', system_id)
end

function PfrService:secure_boot_verify_and_recover()
    local object = self:get_gold_package(bios_enum.SINGLE_HOST_SYSTEM_ID)
    if not object then
        log:notice('[PfrService]PfrService: has no gold package, no need verify.')
        return
    end
    local res = object:secure_boot_verify_and_recover()
    if not res then
        log:error('[PfrService]PfrService: secure boot and recover fail')
    else
        log:notice('[PfrService]PfrService: secure boot and recover success')
        return true
    end
end

-- 检测、自愈
-- 按理: 检测时候,不能升级;升级时候,不能检测
-- a）bios接收到BeforePowerOnSignal信号之后，加上电锁
-- b）判断上下电状态，如果是下电状态则执行c）；否则一直等待下电再执行c）
-- c）通过m3进行对bios得flash进行校验，校验成功执行d），校验失败执行e）
-- d）释放上电锁
-- e）进行自愈升级，如果自愈三次失败，则不允许带内上电，需要更新锁为永久锁
function PfrService:start(system_id)
    log:notice('[PfrService]PfrService: system %s wait power off state.', system_id)
    local ok = self:wait_until_power_off(system_id)
    if not ok then
        log:notice('[PfrService]PfrService: system %s wait power off timeout', system_id)
        return
    end
    log:notice('[PfrService]PfrService: system %s start verify.', system_id)
    local object = self:get_gold_package(system_id)
    if not object then
        log:notice('[PfrService]PfrService: system %s has no gold package, no need verify.', system_id)
        return
    end
    local bios_ser = bios_factory.get_service('bios_service')
    local flash_channel_ids = bios_ser:get_prop(obj_def.FLASH_CHANNEL_IDS, system_id) or {}
    if flash_channel_ids and #flash_channel_ids > 0 then
        local channel_id = flash_channel_ids[1] or 0
        bios_ser:set_prop(obj_def.FLASH_CHANNEL, channel_id, system_id)
    end
    local res = object:verify_and_recover()
    -- 自愈失败场景,相当于pfr hpm包无法自愈,因此需要加永久锁,需要重新升级进行解锁
    if not res then
        error('[PfrService]PfrService: verify and recover fail, power forever lock.')
    else
        log:notice('[PfrService]PfrService: system %s verify and recover success, power unlock.', system_id)
        return
    end
end

function PfrService:lock_fructl(system_id)
    local lock = self:get_power_lock(system_id)
    log:notice('[PfrService]PfrService: system %s start lock fructl.', system_id)
    lock:lock_until_success(FRUCTL_LOCK_TIME, false, 'VerifyFlash')
    log:notice('[PfrService]PfrService: system %s lock fructl success.', system_id)
    return lock
end

local Period3<const> = 1
local function get_periond(system_id)
    local upgrade_ser = bios_factory.get_service('upgrade_service')
    local snapshot = upgrade_ser:get_package_snapshot(system_id)
    if not snapshot then
        log:notice('[PfrService]system %s has no snapshot', system_id)
        return
    end
    return snapshot:get_period()
end

function PfrService:pfr_verify(system_id)
    if get_periond(system_id) ~= Period3 then
        log:notice('[PfrService]PfrService:system %s period dismatch, no need verify', system_id)
        return
    end
    local lock = self:lock_fructl(system_id)
    local ok, err = pcall(PfrService.start, self, system_id)
    if not ok then
        log:error('[PfrService]PfrService:system %s err(%s)', system_id, err)
        -- 加永久锁
        lock:lock_forever()
        self.forever_lock_collection[system_id] = lock
    else
        lock:unlock()
    end
end

function PfrService:start_verify(system_id)
    system_id = system_id or SYSTEM_ID
    if self.running[system_id] then
        log:error('[bios]system %s bios verifying, no need continue verify', system_id)
        return
    end
    self.running[system_id] = true
    local ok, err = pcall(function()
        self.state_machine:lock(10, 'bios')
        self:pfr_verify(system_id)
    end)
    if not ok then
        log:notice('[bios]err is %s, system %s skip the pfr', err, system_id)
    end
    pcall(function()
        self.state_machine:unlock('bios')
    end)
    self.running[system_id] = false
end

function PfrService:cache_hpm(system_id, hpm_path)
    local gold_packages = {}
    if system_id == bios_enum.ALL_HOST_SYSTEM_ID then
        gold_packages = self:get_gold_package_collection()
    else
        table.insert(gold_packages, self:get_gold_package(system_id))
    end
    for _, object in pairs(gold_packages) do
        object:cache_hpm(hpm_path)
    end
end

function PfrService:backup_hpm(system_id)
    local object = self:get_gold_package(system_id or SYSTEM_ID)
    if not object then
        log:error('[backup_hpm]gold package object not found, system id: %s', system_id)
        return
    end
    return object:backup_hpm()
end

function PfrService:remove_tmp_hpm(system_id)
    local object = self:get_gold_package(system_id or SYSTEM_ID)
    if not object then
        log:error('[remove_tmp_hpm]gold package object not found, system id: %s', system_id)
        return
    end
    return object:remove_tmp_hpm()
end

function PfrService:get_bios_gold_valid(req, ctx)
    local system_id = ctx.HostId or 1
    if not req or not ctx then
        log:error('[bios]get_bios_gold_valid: invalid param.')
        return msg.GetBiosGoldValidRsp.new(comp_code.InvalidCommand, GOLD_INVALID, '')
    end
    local object = self:get_gold_package(system_id)
    if not object then
        log:error('[bios]get_bios_gold_valid: gold package is nil, system id is %s.', system_id)
        return msg.GetBiosGoldValidRsp.new(comp_code.InvalidCommand, GOLD_INVALID, '')
    end
    local status = object:get_bios_gold_valid()
    log:notice('[bios]get_bios_gold_valid: status is %s', status)
    return msg.GetBiosGoldValidRsp.new(comp_code.Success, status, '')
end

function PfrService:can_effective(system_id)
    skynet.sleep(200)
    local cfg = bios_cfg.get_instance()
    if cfg:get_prop('SystemStartupState', system_id) == START_FINISH then
        return true
    end
    return false
end

function PfrService:try_get_version(snapshot)
    local system_id = snapshot:get_system_id()
    for _ = 1, 30 do
        local cfg = bios_cfg.get_instance()
        -- 此处只有在无感升级、单host场景下才会执行，因此system id为1
        local version = cfg:get_prop('Version', system_id)
        cfg:fetch_info(system_id)
        if version == snapshot:get_version() then
            return true
        end
        skynet.sleep(300)
    end
    return false
end

function PfrService:wait_effective(snapshot)
    local system_id = snapshot:get_system_id()
    if not self:can_effective(system_id) then
        log:notice('[bios]system %s state not support effective', system_id)
        return
    end
    log:notice('[bios]pfr: system %s start wait effective bios', system_id)
    skynet.sleep(3000)
    local ok, res = pcall(PfrService.try_get_version, self, snapshot)
    if ok and res then
        log:notice('[bios]pfr: system %s activate and effective bios success', system_id)
        snapshot:set_effective(bios_enum.ActivateStatus.Finish)
        self:backup_hpm(system_id)
    else
        log:error('[bios]pfr: system %s activate and effective bios fail, reason is %s', system_id, res)
        snapshot:set_effective(bios_enum.ActivateStatus.Begin)
        self:remove_tmp_hpm(system_id)
    end
end

function PfrService:cache_hpm_after_activate(snapshot)
    if not snapshot then
        log:notice('[bios]cache hpm after activate, get snapshot fail')
        return
    end

    if snapshot:get_package_type() == bios_enum.PackageType.Patch and
        snapshot:get_effective_flag() == bios_enum.ActivateStatus.Running then
        skynet.fork_once(function()
            self:wait_effective(snapshot)
        end)
    else
        log:notice('[bios]pfr: system %s no need wait bios effective', snapshot:get_system_id())
    end
end

return singleton(PfrService)